Visualize preferential data using ternary plots in 2D and higher dimensions
ABC News shows interesting plot on 2025 House of Rep’s distribution of first preference
But some challenges with this plot:
=> prefviz creates a uniform way of visualizing ternary plot of any dimensions
First Preference Distribution
Hover to see electorate names and exact vote percentages.
Full Preference Flow
Track how votes move between parties as candidates are eliminated.
For elections with 4+ significant parties, we integrate with tourr and detourr for dynamic rotations through preference space.
prefviz functions| Step | Data Transformation | Extract Ternary Components | Visualization |
|---|---|---|---|
| What it does | Convert raw ballot data to aggregated compositional percentages | Build geometric infrastructure (vertices, edges, coordinates) | Interactive 2D or high-dimensional plots |
| Key functions | dop_irv() – for raw ballot datadop_transform() – reshape aggregated data |
ternable() – create ternableobjectGetter functions for transforming components to appropriate shapes |
ggplot2 + plotly (2D)tourr + detourr (high-D) |
| Input | Raw ballots or aggregated preferences | Standardized compositional data | Ternary components from ternable object |
| Output | Clean, standardized format | Geometric objects ready to plot | Interactive, explorable visualization |
AEC’s 2025 House of Representatives distribution of preferences is in aggregated percentage, with each row representing the preference of a party in an electorate.
pref25_2d <- aecdop_2025 |>
filter(CalculationType == "Preference Percent") |>
mutate(Party = case_when(
!(PartyAb %in% c("LP", "ALP", "NP", "LNP", "LNQ")) ~ "Other",
PartyAb %in% c("LP", "NP", "LNP", "LNQ") ~ "LNP",
TRUE ~ PartyAb
))
head(pref25_2d)# A tibble: 6 × 15
StateAb DivisionID DivisionNm CountNumber BallotPosition CandidateID Surname
<chr> <dbl> <chr> <dbl> <dbl> <dbl> <chr>
1 ACT 318 Bean 0 1 41076 LAMERTON
2 ACT 318 Bean 0 2 41682 PRICE
3 ACT 318 Bean 0 3 41436 CARTER
4 ACT 318 Bean 0 4 40676 SMITH
5 ACT 318 Bean 1 1 41076 LAMERTON
6 ACT 318 Bean 1 2 41682 PRICE
# ℹ 8 more variables: GivenNm <chr>, PartyAb <chr>, PartyNm <chr>,
# Elected <chr>, HistoricElected <chr>, CalculationType <chr>,
# CalculationValue <dbl>, Party <chr>
Transformed data with 3 columns representing the composition of each party in each electorate, summing to 1.
pref25_2d <- dop_transform(
data = pref25_2d,
key_cols = c(DivisionNm, CountNumber),
value_col = CalculationValue,
item_col = Party,
winner_col = Elected,
winner_identifier = "Y"
)
pref25_2d# A tibble: 976 × 6
DivisionNm CountNumber ALP LNP Other Winner
<chr> <dbl> <dbl> <dbl> <dbl> <chr>
1 Adelaide 0 0.465 0.242 0.294 ALP
2 Adelaide 1 0.467 0.242 0.291 ALP
3 Adelaide 2 0.476 0.244 0.279 ALP
4 Adelaide 3 0.483 0.249 0.268 ALP
5 Adelaide 4 0.493 0.285 0.222 ALP
6 Adelaide 5 0.691 0.309 0 ALP
7 Aston 0 0.373 0.377 0.251 ALP
8 Aston 1 0.373 0.378 0.249 ALP
9 Aston 2 0.376 0.380 0.244 ALP
10 Aston 3 0.378 0.384 0.238 ALP
# ℹ 966 more rows
ternable() creates a ternable object, which is a S3 object that contains the data and metadata necessary for ternary plots, including the vertices, edges, labels of the simplex, and coordinates of all data points.
List of 6
$ data : tibble [976 × 6] (S3: tbl_df/tbl/data.frame)
..$ DivisionNm : chr [1:976] "Adelaide" "Adelaide" "Adelaide" "Adelaide" ...
..$ CountNumber: num [1:976] 0 1 2 3 4 5 0 1 2 3 ...
..$ ALP : num [1:976] 0.465 0.467 0.476 0.483 0.493 ...
..$ LNP : num [1:976] 0.242 0.242 0.244 0.249 0.285 ...
..$ Other : num [1:976] 0.294 0.291 0.279 0.268 0.222 ...
..$ Winner : chr [1:976] "ALP" "ALP" "ALP" "ALP" ...
$ ternary_coord : tibble [976 × 2] (S3: tbl_df/tbl/data.frame)
..$ x1: num [1:976] 0.158 0.159 0.164 0.165 0.147 ...
..$ x2: num [1:976] 0.0487 0.0522 0.0662 0.0801 0.1367 ...
$ data_edges : int [1:976, 1:2] 1 2 3 4 5 6 7 8 9 10 ...
..- attr(*, "dimnames")=List of 2
.. ..$ : NULL
.. ..$ : chr [1:2] "Var1" "Var2"
$ simplex_vertices: tibble [3 × 3] (S3: tbl_df/tbl/data.frame)
..$ x1 : num [1:3] 0.707 -0.707 0
..$ x2 : num [1:3] 0.408 0.408 -0.816
..$ labels: chr [1:3] "ALP" "LNP" "Other"
$ simplex_edges : int [1:6, 1:2] 2 3 1 3 1 2 1 1 2 2 ...
..- attr(*, "dimnames")=List of 2
.. ..$ : chr [1:6] "2" "3" "4" "6" ...
.. ..$ : chr [1:2] "Var1" "Var2"
$ vertex_labels : chr [1:3] "ALP" "LNP" "Other"
- attr(*, "class")= chr "ternable"
prefviz includes some ggplot2 extensions to make creating ternary plots easier. Output is compatible with plotly and ggiraph.
input_data <- get_tern_data(tern_2d, plot_type = "2D") |>
mutate(text = paste0(DivisionNm, "\n",
"ALP: ", round(ALP, 1), "%\n",
"LNP: ", round(LNP, 1), "%\n",
"Other: ", round(Other, 1), "%"))
p2d <- ggplot(input_data |> filter(CountNumber == 0), aes(x = x1, y = x2)) +
geom_ternary_cart() +
geom_ternary_region(
aes(fill = after_stat(vertex_labels)),
x1 = 1/3, x2 = 1/3, x3 = 1/3,
vertex_labels = tern_2d$vertex_labels,
alpha = 0.3, color = NA, show.legend = FALSE
) +
add_vertex_labels(tern_2d$simplex_vertices) +
geom_point(aes(color = Winner, text = text)) +
scale_fill_manual(
values = c("ALP" = "red", "LNP" = "blue", "Other" = "grey70")
) +
scale_color_manual(
values = c("ALP" = "red", "LNP" = "blue", "Other" = "grey70"),
name = "Elected Party"
) +
labs(title = "First preference in 2022 Australian Federal election")
plotly_ternary <- ggplotly(p2d, tooltip = "text", width = 600, height = 400)prefviz depends on tourr for high-dimensional visualization, and detourr for interactive tours.
Priorities
Potential avenues
detourr integration.